热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

单号|前文_防止重复下单分布式系统接口幂等性实现方案

篇首语:本文由编程笔记#小编为大家整理,主要介绍了防止重复下单分布式系统接口幂等性实现方案相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了防止重复下单分布式系统接口幂等性实现方案相关的知识,希望对你有一定的参考价值。




问题背景


最容易想到的使用数据库事务
比如创建订单时,要同时往订单表、订单商品表插入数据,那这些插入数据的INSERT必须在一个数据库事务执行,数据库事务可保证:执行这些INSERT语句,同生共死!

订单服务调用支付服务,不巧网络超时,然后订单服务走重试机制,给你重试了一把,支付服务收到一个支付请求两次,而且因为轮询负载均衡算法落在不同机器!
所以一个分布式系统中的接口,要如何保证幂等性呢?



如何避免重复下单?


评论里有同学说,前端页面直接防止用户重复提交表单。没啥毛病,但网络错误会导致重传,很多RPC框架、网关都有自动重试机制,所以重复请求无法避免。问题最终还是归结于如何保证服务接口的幂等性。



如何判断请求是重复的?



  • 插入订单前,先查一下订单表里面有无重复订单?
    这可不好,你很难用SQL条件定义什么是“重复订单”

  • 订单的用户、商品、价格一样就是重复订单?
    万一这用户就是连续下了俩一模一样订单呢!

所以保证幂等性要做到如下几点!



每个请求须有唯一标识


比如订单支付请求,得包含订单id,一个订单id最多支付一次!



每次处理完请求后,须有记录标识该请求已被处理


mysql中记录一个状态字段。比如支付之前记录一条这个订单的支付流水。



每次接收请求判断之前是否处理过


若有一个订单已支付,就已经有了一条支付流水。若重复发送这个请求,则此时先插入支付流水,orderId已存在,唯一键约束生效,报错插入不进去。就不会再重复扣款。

在往db插条记录时,一般不提供主键,而由数据库在插入时自动生成一个主键。这样重复的请求就会导致插入重复数据。
MySQL的主键自带唯一性约束,若在一条INSERT语句提供主键,且该主键值在表中已存在,则该条INSERT会执行失败。因此可利用db的“主键唯一约束”,在插数据时带上主键,以此实现创建订单接口的幂等性。

给订单服务添加一个“orderId生成”的接口,无参,返回值就是一个全局唯一订单号。在用户进入创建订单页面时,前端页面先调用该orderId生成接口得到一个订单号,在用户提交订单的时候,在创建订单的请求中携带该订单号。

该订单号其实就是订单表的主键,如此一来,重复请求中带的都是同一订单号。订单服务在订单表中插入数据的时候,执行的这些重复INSERT语句中的主键,也都是同一个订单号。而数据库的唯一约束可保证,只有一次INSERT执行成功。

实际要结合业务,比如使用Redis,用orderId作为唯一键。只有成功插入这个支付流水,才可执行扣款。

要求是支付一个订单,必须插入一条支付流水,order_id建立一个唯一键。
你在支付一个订单前,先插入一条支付流水,order_id就已经传过去了。就可以写一个标识到Redis中,set order_id payed,当重复请求过来时,先查Redis的order_id对应的value,若为payed说明已支付,就别重复支付了!

然后再重复支付订单时,写尝试插入一条支付流水,db会报唯一键冲突,整个事务回滚即可。
保存一个是否处理过的标识也可以,服务的不同实例可以一起操作Redis。


  • 幂等创建订单的时序图

如果因为重复订单导致插入订单表失败,订单服务不要把这个错误返回给前端页面。否则,就可能出现用户点击创建订单按钮后,页面提示创建订单失败,而实际上订单却创建成功了。正确的做法是,遇到这种情况,订单服务直接返回订单创建成功即可。


3 怎么解决ABA问题?

3.1 什么是 ABA?

比如订单支付后,卖家要发货,发货完成后要填个快递单号。假设说,卖家填了个666,刚填完,发现填错了,赶紧再修改成888。对订单服务来说,这就是2个更新订单的请求。系统异常时666请求到了,单号更成666,接着888请求到了,单号又更新成888,但是666更新成功的响应丢了,调用方没收到成功响应,自动重试,再次发起666请求,单号又被更新成666了,这数据显然就错了.


  • 时序图

3.2 解决方案


通用的解决方案

订单主表增加一列version。每次查询订单的时候,版本号要随着订单数据返回给页面。
页面在更新数据的请求中,把这个版本号作为更新请求的参数,带回给订单更新接口。

订单服务在更新数据的时候,需要比较订单的版本号是否和消息中的一致:


  • 不一致 拒绝更新数据
  • 一致 还需要再更新数据的同时,把版本号+1。“比较版本号、更新数据和版本号+1”,这个过程必须在同一个事务里面执行。

UPDATE orders set tracking_number = 666, version = version + 1
WHERE version = 8;

在这条SQL的WHERE条件中,version的值需要页面在更新的时候通过请求传进来。

通过这个版本号,就可以保证,从我打开这条订单记录开始,一直到我更新这条订单记录成功,这个期间没有其他人修改过这条订单数据。因为,如果有其他人修改过,数据库中的版本号就会改变,那我的更新操作就不会执行成功。我只能重新查询新版本的订单数据,然后再尝试更新。

有了这个版本号,前文的ABA即有两个 case


  • 把运单号更新为666的操作成功了,更新为888的请求带着旧版本号,那就会更新失败,页面提示用户更新888失败
  • 第二种情况,666更新成功后,888带着新的版本号,888更新成功。这时候即使重试的666请求再来,因为它和上一条666请求带着相同的版本号,上一条请求更新成功后,这个版本号已经变了,所以重试请求的更新必然失败

无论哪种情况,数据库中的数据与页面上给用户的反馈都是一致的。这样就可以实现幂等更新并且避免了ABA问题


  • 下图展示case1

4 总结
  • 对于创建订单服务来说,可以通过预先生成订单号,然后利用数据库中订单号的唯一约束这个特性,避免重复写入订单,实现创建订单服务的幂等性
  • 对于更新订单服务,可以通过一个版本号机制,每次更新数据前校验版本号,更新数据同时自增版本号,这样的方式,来解决ABA问题,确保更新订单服务的幂等性。

两种幂等的实现方法,就可以保证,无论请求是不是重复,订单表中的数据都是正确的。

实现订单幂等的方法,完全可以套用在其他需要实现幂等的服务中,只需要这个服务操作的数据保存在数据库中,并且有一张带有主键的数据表即可


推荐阅读
  • 本文由编程笔记小编整理,介绍了PHP中的MySQL函数库及其常用函数,包括mysql_connect、mysql_error、mysql_select_db、mysql_query、mysql_affected_row、mysql_close等。希望对读者有一定的参考价值。 ... [详细]
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • 篇首语:本文由编程笔记#小编为大家整理,主要介绍了软件测试知识点之数据库压力测试方法小结相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 面试经验分享:华为面试四轮电话面试、一轮笔试、一轮主管视频面试、一轮hr视频面试
    最近有朋友去华为面试,面试经历包括四轮电话面试、一轮笔试、一轮主管视频面试、一轮hr视频面试。80%的人都在第一轮电话面试中失败,因为缺乏基础知识。面试问题涉及 ... [详细]
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • PHP设置MySQL字符集的方法及使用mysqli_set_charset函数
    本文介绍了PHP设置MySQL字符集的方法,详细介绍了使用mysqli_set_charset函数来规定与数据库服务器进行数据传送时要使用的字符集。通过示例代码演示了如何设置默认客户端字符集。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 本文详细介绍了MysqlDump和mysqldump进行全库备份的相关知识,包括备份命令的使用方法、my.cnf配置文件的设置、binlog日志的位置指定、增量恢复的方式以及适用于innodb引擎和myisam引擎的备份方法。对于需要进行数据库备份的用户来说,本文提供了一些有价值的参考内容。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 如何在php中将mysql查询结果赋值给变量
    本文介绍了在php中将mysql查询结果赋值给变量的方法,包括从mysql表中查询count(学号)并赋值给一个变量,以及如何将sql中查询单条结果赋值给php页面的一个变量。同时还讨论了php调用mysql查询结果到变量的方法,并提供了示例代码。 ... [详细]
  • MyBatis多表查询与动态SQL使用
    本文介绍了MyBatis多表查询与动态SQL的使用方法,包括一对一查询和一对多查询。同时还介绍了动态SQL的使用,包括if标签、trim标签、where标签、set标签和foreach标签的用法。文章还提供了相关的配置信息和示例代码。 ... [详细]
  • 一次上线事故,30岁+的程序员踩坑经验之谈
    本文主要介绍了一位30岁+的程序员在一次上线事故中踩坑的经验之谈。文章提到了在双十一活动期间,作为一个在线医疗项目,他们进行了优惠折扣活动的升级改造。然而,在上线前的最后一天,由于大量数据请求,导致部分接口出现问题。作者通过部署两台opentsdb来解决问题,但读数据的opentsdb仍然经常假死。作者只能查询最近24小时的数据。这次事故给他带来了很多教训和经验。 ... [详细]
  • 本文介绍了关系型数据库和NoSQL数据库的概念和特点,列举了主流的关系型数据库和NoSQL数据库,同时描述了它们在新闻、电商抢购信息和微博热点信息等场景中的应用。此外,还提供了MySQL配置文件的相关内容。 ... [详细]
  • 2021最新总结网易/腾讯/CVTE/字节面经分享(附答案解析)
    本文分享作者在2021年面试网易、腾讯、CVTE和字节等大型互联网企业的经历和问题,包括稳定性设计、数据库优化、分布式锁的设计等内容。同时提供了大厂最新面试真题笔记,并附带答案解析。 ... [详细]
author-avatar
mzyzzyk
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有